package com.gitee.wsl.data.input

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first

/**
 * A wrapper type designed to be stored within a Repository ViewModel and observed as a Flow in a Fragment ViewModel,
 * for fetching data from a remote source and caching it in memory. When data is refreshed, the previous values are kept
 * around for a nicer user experience, so the entire screen's data does not get cleared and re-populated causing
 * unwanted "jank" in the UI.
 *
 * StateCached values should only be updated from a Repository. Typically, a UI ViewModel will observe a specific StateCached
 * value, and the act of observing it requests the Repository start loading it. The UI ViewModel is then just a passive
 * observer of the state of the StateCached value, which encapsulates all the data needed to decide when to show progress
 * loaders or success/error states of that value. When a UI ViewModel observes a StateCached value, it should not need any
 * additional bookkeeping.
 */
sealed class StateCached<T> {

    /**
     * The default state of a StateCached value. StateCached values in a ViewModel or Repository State should never be null, but
     * always initialized to [StateCached.NotLoaded].
     *
     * Forcing a refresh will reset the property's state back to NotLoaded, which will then call to the remote data
     * source again.
     */
     class NotLoaded<T>( val previousCachedValue: T? = null) : StateCached<T>() {
        override fun toString(): String {
            return "NotLoaded(previousCachedValue=$previousCachedValue)"
        }
    }

    /**
     * Indicates that we have started the call to the remote data source, but it has not responded or failed yet. The
     * "fetcher" coroutine is still active.
     */
     class Fetching<T>( val cachedValue: T?) : StateCached<T>() {
        override fun toString(): String {
            return "Fetching(cachedValue=$cachedValue)"
        }
    }

    /**
     * Indicates that the remote "fetcher" has sucessfully returned data and that the Repository successfully performed
     * any follow-up computation on that data.
     */
     class Value<T>( val value: T) : StateCached<T>() {
        override fun toString(): String {
            return "Value(value=$value)"
        }
    }

    /**
     * Indicates that either the remote "fetcher" function itself failed, or that it succeeded but returned bad data
     * that caused the Repository to throw an exception when processing it.
     */
     class FetchingFailed<T>( val error: Throwable,  val cachedValue: T?) : StateCached<T>() {
        override fun toString(): String {
            return "FetchingFailed(error=${error.message}, cachedValue=$cachedValue)"
        }
    }
}

/**
 * Get the value if the remote data source returned valid data. This method will not return previously StateCached values
 * once the cache has started refreshing, but instead will return the result of [defaultValue].
 */
 fun <T> StateCached<T>.getValueOrElse(defaultValue: () -> T): T {
    return when (this) {
        is StateCached.Fetching -> defaultValue()
        is StateCached.FetchingFailed -> defaultValue()
        is StateCached.NotLoaded -> defaultValue()
        is StateCached.Value -> value
    }
}

/**
 * Get the value if the remote data source returned valid data. This method will not return previously StateCached values
 * once the cache has started refreshing, but instead will return null.
 */
 fun <T> StateCached<T>.getValueOrNull(): T? {
    return when (this) {
        is StateCached.Fetching -> null
        is StateCached.FetchingFailed -> null
        is StateCached.NotLoaded -> null
        is StateCached.Value -> value
    }
}

/**
 * Get the value if the remote data source returned valid data. This method will not return previously StateCached values
 * once the cache has started refreshing, but instead will throw an Exception.
 */
 fun <T> StateCached<T>.getValueOrThrow(): T {
    return when (this) {
        is StateCached.Fetching -> throw NullPointerException("Expected a StateCached value, got Fetching")
        is StateCached.FetchingFailed -> throw NullPointerException("Expected a StateCached value, got FetchingFailed")
        is StateCached.NotLoaded -> throw NullPointerException("Expected a StateCached value, got NotLoaded")
        is StateCached.Value -> value
    }
}

/**
 * Get the value if the remote data source returned valid data. This method will not return previously StateCached values
 * once the cache has started refreshing, but instead will return null.
 */
 fun <T> StateCached<List<T>>.getValueOrEmptyList(): List<T> {
    return when (this) {
        is StateCached.Fetching -> emptyList()
        is StateCached.FetchingFailed -> emptyList()
        is StateCached.NotLoaded -> emptyList()
        is StateCached.Value -> value
    }
}

/**
 * Get the value if the remote data source returned valid data. If the cache is currently refreshing, this will return
 * the previous StateCached value so the UI can continue displaying it while it adds a progress indicator over it. If there
 * was no previosuly StateCached value to be displayed, return the result of [defaultValue].
 */
 fun <T> StateCached<T>.getCachedOrElse(defaultValue: () -> T): T {
    return when (this) {
        is StateCached.Fetching -> cachedValue ?: defaultValue()
        is StateCached.FetchingFailed -> cachedValue ?: defaultValue()
        is StateCached.NotLoaded -> previousCachedValue ?: defaultValue()
        is StateCached.Value -> value
    }
}

/**
 * Get the value if the remote data source returned valid data. If the cache is currently refreshing, this will return
 * the previous StateCached value so the UI can continue displaying it while it adds a progress indicator over it. If there
 * was no previosuly StateCached value to be displayed, return null.
 */
 fun <T> StateCached<T>.getCachedOrNull(): T? {
    return when (this) {
        is StateCached.Fetching -> cachedValue
        is StateCached.FetchingFailed -> cachedValue
        is StateCached.NotLoaded -> previousCachedValue
        is StateCached.Value -> value
    }
}

/**
 * Get the value if the remote data source returned valid data. If the cache is currently refreshing, this will return
 * the previous StateCached value so the UI can continue displaying it while it adds a progress indicator over it. If there
 * was no previosuly StateCached value to be displayed, throw an exception.
 */
 fun <T> StateCached<T>.getCachedOrThrow(): T {
    return when (this) {
        is StateCached.Fetching -> {
            cachedValue ?: throw NullPointerException("Expected a StateCached value, got Fetching")
        }
        is StateCached.FetchingFailed -> {
            cachedValue ?: throw NullPointerException("Expected a StateCached value, got FetchingFailed")
        }
        is StateCached.NotLoaded -> {
            previousCachedValue ?: throw NullPointerException("Expected a StateCached value, got NotLoaded")
        }
        is StateCached.Value -> {
            value
        }
    }
}

/**
 * Get the value if the remote data source returned valid data. If the cache is currently refreshing, this will return
 * the previous StateCached value so the UI can continue displaying it while it adds a progress indicator over it. If there
 * was no previosuly StateCached value to be displayed, return the result of [defaultValue].
 */
 fun <T> StateCached<List<T>>.getCachedOrEmptyList(): List<T> {
    return when (this) {
        is StateCached.Fetching -> cachedValue ?: emptyList()
        is StateCached.FetchingFailed -> cachedValue ?: emptyList()
        is StateCached.NotLoaded -> previousCachedValue ?: emptyList()
        is StateCached.Value -> value
    }
}

/**
 * Returns true if this StateCached variable indicates a state which should show a progress indicator in the UI.
 */
 fun <T> StateCached<T>.isLoading(): Boolean {
    return when (this) {
        is StateCached.Fetching -> true
        is StateCached.FetchingFailed -> false
        is StateCached.NotLoaded -> true
        is StateCached.Value -> false
    }
}

/**
 * Returns true if all StateCached properties are not in the "Loading" state
 */
 fun isReady(vararg StateCached: StateCached<*>): Boolean {
    return StateCached
        .map { !it.isLoading() }
        .all { it }
}

/**
 * Returns true if the Repository has not started fetching from the remote source yet, or if it has started fetching and
 * did not have a prior value (thus, is the first time attempting to load this value).
 */
 fun <T> StateCached<T>.isFirstLoad(): Boolean {
    return when (this) {
        is StateCached.Fetching -> this.cachedValue == null
        is StateCached.FetchingFailed -> false
        is StateCached.NotLoaded -> true
        is StateCached.Value -> false
    }
}

/**
 * Determines whether the value of this cache is considered "valid", according to the provided [validator] function. The
 * cache is considered invalid when fetching failed, or when fetching completed but fails to pass the [validator]
 * function. Data that is currently loading is considered valid.
 */
 fun <T> StateCached<T>.isValid(validator: (T) -> Boolean): Boolean {
    return when (this) {
        is StateCached.Fetching -> true
        is StateCached.FetchingFailed -> false
        is StateCached.NotLoaded -> true
        is StateCached.Value -> validator(value)
    }
}

/**
 * Execute [block] if this StateCached value is [StateCached.FetchingFailed].
 */
 fun <T> StateCached<T>.onFailure(block: (Throwable) -> Boolean): StateCached<T> {
    return when (this) {
        is StateCached.Fetching -> this
        is StateCached.FetchingFailed -> this.also { block(error) }
        is StateCached.NotLoaded -> this
        is StateCached.Value -> this
    }
}

/**
 * Unwrap the StateCached value, apply the [transform] function to it if there is a current or previously-StateCached value, and
 * then wrap it in the same status.
 */
 fun <T, U> StateCached<T>.map(transform: (T) -> U): StateCached<U> {
    return when (this) {
        is StateCached.Fetching -> {
            runCatching { StateCached.Fetching(cachedValue?.let { transform(it) }) }
                .getOrElse { StateCached.FetchingFailed<U>(it, null) }
        }
        is StateCached.FetchingFailed -> {
            runCatching { StateCached.FetchingFailed(error, cachedValue?.let { transform(it) }) }
                .getOrElse { StateCached.FetchingFailed<U>(it, null) }
        }
        is StateCached.NotLoaded -> {
            runCatching { StateCached.NotLoaded(previousCachedValue?.let { transform(it) }) }
                .getOrElse { StateCached.FetchingFailed<U>(it, null) }
        }
        is StateCached.Value -> {
            runCatching { StateCached.Value(transform(value)) }
                .getOrElse { StateCached.FetchingFailed<U>(it, null) }
        }
    }
}

/**
 * Wait for the cache to finish loading, and return the result.
 */
 suspend fun <T> Flow<StateCached<T>>.awaitValue(): StateCached<T> {
    return this.first { it is StateCached.Value || it is StateCached.FetchingFailed }
}

